package com.atguigu.serurity.filter;

import com.atguigu.commonutils.R;
import com.atguigu.commonutils.ResponseUtil;
import com.atguigu.serurity.security.TokenManager;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.util.StringUtils;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 * <p>
 * 权限认证过滤器，用于对用户权限进行认证
 * </p>
 *
 * @author hwt
 * @since 2022-10-9
 */
public class TokenAuthenticationFilter extends BasicAuthenticationFilter {

    // 自定义 token 加密工具
    private final TokenManager tokenManager;
    // redis
    private final RedisTemplate<String, List<String>> redisTemplate;

    // 构造器，手动注入
    public TokenAuthenticationFilter(AuthenticationManager authManager,
                                     TokenManager tokenManager, RedisTemplate<String, List<String>> redisTemplate) {
        // 将 通过 authenticationManager() 方法 获取到的 认证管理器，传递给 BasicAuthenticationFilter 父类进行保存
        super(authManager);
        this.tokenManager = tokenManager;
        this.redisTemplate = redisTemplate;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
            throws IOException, ServletException {

        logger.info("=======请求地址==========" + req.getRequestURI());

        // 判断是否是要访问后台接口，不是就放行，是就进行后面的验证
        // contains(): 用于判断目标字符串中，是否包含指定字符串
        if (!req.getRequestURI().contains("admin"))  chain.doFilter(req, res);


        // 定义 Authentication 子类对象，用于接收：获取到权限列表的用户信息
        UsernamePasswordAuthenticationToken authentication = null;

        // 进行获取用户权限列表，如果获取不到用户的权限列表，则返回 null
        authentication = getAuthentication(req);

        // 返回的不为 null ，表示获取权限列表成功
        // 判断成功，表示权限获取成功了，并且保存到了上面创建的 Authentication 子类对象中
        // 然后获取到 “上下文对象” 替换当前用户的主体信息为最新的，拥有权限列表的 authentication
        if (authentication != null) SecurityContextHolder.getContext().setAuthentication(authentication);

        // 如果获取到的为空，则输出错误信息
        else ResponseUtil.out(res, R.error().message("鉴权失败"));

        // 不管成功还是失败，都会进行放行，但是如果失败了，权限列表为空，访问权限接口，就会出现认证错误
        chain.doFilter(req, res);
    }

    /**
     * 进行 token 校验
     *
     * @param request 请求对象
     * @return 认证成功后的 Authentication 子类对象
     */
    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
        // token 置于 header里
        String token = request.getHeader("token");

        // 判断 token 不同为空
        if (token != null && !"".equals(token.trim())) {

            // 使用自定义的 token 管理器，解析 token 中的值，并获取到 name
            String userName = tokenManager.getUserFromToken(token);

            // 根据解析到的 用户名 获取到缓存中的用户权限信息，如果缓存中存在，则是已经登录的用户
            List<String> permissionValueList = redisTemplate.opsForValue().get(userName);

            // 如果从缓存中获取的信息为空，则直接返回空，本次调用结束
            if (permissionValueList == null) return null;

            // 不为空，则定义 Collection 对象，这个对象是所有单列集合的最顶层的接口,里边定义了所有单列集合共性的方法
            Collection<GrantedAuthority> authorities = new ArrayList<>();

            // 遍历获取到的用户信息集合
            for (String permissionValue : permissionValueList) {

                // 判断集合中取到的值是否为空，如果为空，则跳过本次循环
                if (StringUtils.isEmpty(permissionValue)) continue;

                // 如果获取到的用户信息不是空，封装为权限对象
                SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);

                // 将权限对象，存放到权限列表中
                authorities.add(authority);
            }

            // 创建新的 Authentication 子类对象 并进行返回，只不过这里设置了权限集合数组
            return new UsernamePasswordAuthenticationToken(userName, token, authorities);
        }

        // 进入到这里，则表示 token 为空，直接返回 空 即可
        return null;
    }
}